// Copyright lowRISC contributors (OpenTitan project).
// Licensed under the Apache License, Version 2.0, see LICENSE for details.
// SPDX-License-Identifier: Apache-2.0
//
// Collects sequence items from observed I2C bus transactions.
//
// Read and Write transactions are collected into different ports, and also
// different ports are used depending on if the Agent itself is configured as
// either a Controller or Target.
// Agent-Target
// W -> 'target_mode_wr_item_port'
// R -> 'target_mode_rd_item_port'
// Agent-Controller
// W -> 'controller_mode_wr_item_port'
// R -> 'controller_mode_rd_item_port'
//
// Seq_items are written to the controller-mode and target-mode 'in_progress_ports', which can
// be picked up by consumers who wish to interrogate the state of a transfer in a more granular
// fashion. This can be used for prediction, or for controlling sequences in a reactive agent
// system.
//
// Methods
//
// > Agent-Controller (cfg.if_mode == Host)
// controller_collect_thread()
// controller_address_thread(), controller_read_thread(), controller_write_thread()
//
// > Agent-Target (cfg.if_mode == Device)
// target_collect_thread()
// target_address_thread(), target_read_thread(), target_write_thread()
//
class i2c_monitor extends dv_base_monitor #(
    .ITEM_T (i2c_item),
    .CFG_T  (i2c_agent_cfg),
    .COV_T  (i2c_agent_cov)
  );

  /////////////////////
  // CLASS VARIABLES //
  /////////////////////

  // Full-transaction ports
  uvm_analysis_port #(i2c_item) controller_mode_wr_item_port;
  uvm_analysis_port #(i2c_item) controller_mode_rd_item_port;
  uvm_analysis_port #(i2c_item) target_mode_wr_item_port;
  uvm_analysis_port #(i2c_item) target_mode_rd_item_port;

  // In-progress transaction ports
  uvm_analysis_port #(i2c_item) controller_mode_in_progress_port;
  uvm_analysis_port #(i2c_item) target_mode_in_progress_port;


  // This item is used to capture and accumulate ongoing transaction control and data symbols.
  // It is the main piece of state used by a monitor instance to keep track of the ongoing I2C
  // transaction, across all subroutines. Once a transaction has completed, this item is cloned and
  // published to the appropriate TLM ports.
  local i2c_item mon_dut_item;
  local i2c_item prev_item; // Temporarily holds a handle to the previous transfer item

  // Transfer counter (becomes 'ITEM_T.tran_id' for all observed transfers on the bus)
  // This counter is incremented for every transfer seen on the bus, no matter the address or if
  // the transfer is accepted by an i2c_target device. Every seq_item generated by this monitor
  // should have a new unique tran_id.
  local uint num_dut_tran = 0;

  `uvm_component_utils(i2c_monitor)
  `uvm_component_new

  ///////////////////
  // CLASS METHODS //
  ///////////////////

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    controller_mode_wr_item_port = new("controller_mode_wr_item_port", this);
    controller_mode_rd_item_port = new("controller_mode_rd_item_port", this);
    target_mode_wr_item_port = new("target_mode_wr_item_port", this);
    target_mode_rd_item_port = new("target_mode_rd_item_port", this);
    controller_mode_in_progress_port = new("controller_mode_in_progress_port", this);
    target_mode_in_progress_port = new("target_mode_in_progress_port", this);
  endfunction : build_phase

  virtual task wait_for_reset_and_drop_item();
    @(negedge cfg.vif.rst_ni);
    reset_state();
  endtask : wait_for_reset_and_drop_item

  virtual function reset_state();
    num_dut_tran = 0;
    mon_dut_item.clear_all();
  endfunction

  virtual task run_phase(uvm_phase phase);
    `DV_CHECK(cfg.target_addr_mode == Addr7BitMode, "Only 7-bit addressing mode is supported!")
    wait(cfg.en_monitor);
    wait(cfg.vif.rst_ni);
    if (cfg.if_mode == Host) begin
      fork
        forever controller_collect_thread();
      join_none
    end else if (cfg.if_mode == Device) begin
      forever begin
        fork begin: iso_fork
          fork
            target_collect_thread();
            begin // Monitor on-the-fly reset, clear all state if reset asserted
              wait_for_reset_and_drop_item();
              `uvm_info(`gfn, $sformatf("Monitor saw reset assertion, clearing state now:\n%s",
                                        mon_dut_item.sprint()), UVM_DEBUG)
            end
            forever perf_monitor(cfg.vif, cfg.start_perf_monitor, cfg.stop_perf_monitor);
          join_any
          disable fork;
        end: iso_fork join
      end
    end
  endtask : run_phase

  // Monitor SCL to measure the actual frequency of an I2C transaction.
  virtual task automatic perf_monitor(virtual i2c_if vif, ref uvm_event start, ref uvm_event stop);

    `uvm_info(`gfn, "perf_monitor(): Waiting for start event.", UVM_DEBUG)
    start.wait_trigger();
    `uvm_info(`gfn, "perf_monitor(): Got start event.", UVM_DEBUG)

    // Clear out any captured measurements from the previous sample (this limits the size
    // of the queue, and prevents measuring the same period twice)
    cfg.period_q.delete();

    fork begin : iso_fork
      fork
        // Measure the elapsed simulation time between sucessive posedges of SCL. Push each
        // value into the 'period_q[$]' to be consumed elsewhere for checking.
        begin
          realtime last_posedge, current_posedge;
          forever begin
            @(posedge vif.cb.scl_i);
            current_posedge = $realtime;
            if (last_posedge != 0) begin
              cfg.period_q.push_back(time'(current_posedge - last_posedge));
              `uvm_info(`gfn, $sformatf("perf_monitor(): scl_period_observed = %0t",
                                        cfg.period_q[$]), UVM_DEBUG)
            end
            last_posedge = current_posedge;
          end
        end
        // Wait for the trigger event that stops the performance monitor. When this occurs,
        // join and disable the parallel monitoring process.
        begin
          stop.wait_trigger();
          `uvm_info(`gfn, "perf_monitor(): Got stop event.", UVM_DEBUG)
        end
      join_any
      disable fork;
    end : iso_fork join
  endtask


  virtual protected task target_collect_thread();

    // Wait for the driver/vseq to clear this at the start of the next stimulus round.
    wait(cfg.got_stop == 0);

    `uvm_create_obj(i2c_item, mon_dut_item);

    // Publish a handle of the in-progress transfer via the in_progress port.
    // This can be used to make fine-grained predictions based on the ongoing state of the bus,
    // such as by a predictor/reference_model.
    // Note. that any consumers of this item should not modify it.
    target_mode_in_progress_port.write(mon_dut_item);

    // Decode the start of the next transfer. As we are calling this task in a loop, this may be
    // the start of a new transaction, or the start of new transfer following a repeated-start
    // condition.

    if (prev_item != null && prev_item.rstart_back) begin

      // The previous transaction ended with an RSTART
      mon_dut_item.rstart_front = 1'b1;

    end else if (// Wait for the next START condition if...
                 // - This is the first transaction we are capturing, OR...
                 prev_item == null ||
                 // - The previous captured transaction ended with a STOP.
                 prev_item.stop) begin

      `uvm_info(`gfn, "target_collect_thread(): Waiting for START condition.", UVM_FULL)
      cfg.vif.wait_for_host_start(cfg.timing_cfg);
      `uvm_info(`gfn, "target_collect_thread(): Saw START condition.", UVM_FULL)

      mon_dut_item.start = 1'b1;

    end else begin
      // If we got to the else-condition, it means our decoding above failed, and somehow the
      // items are in an unexpected state. This may be a testbench bug.
      `uvm_fatal(`gfn, "State at start of target_collect_thread() is unexpected!")
    end

    mon_dut_item.state = i2c_agent_pkg::StStarted;
    mon_dut_item.tran_id = num_dut_tran++;

    // Capture the transfer
    begin
      target_address_thread();
      case (mon_dut_item.bus_op)
        BusOpRead: target_read_thread();
        BusOpWrite: target_write_thread();
        default: `uvm_fatal(`gfn, "Should never get here!")
      endcase
    end

    mon_dut_item.state = i2c_agent_pkg::StStopped;

    // If the previous transfer ended via STOP (as opposed to RSTART), signal this to the driver.
    if (mon_dut_item.stop) cfg.got_stop = 1;

    // The transfer has ended. Send the completed item to the scoreboard.
    begin
      i2c_item full_item;
      `downcast(full_item, mon_dut_item.clone());
      case (full_item.bus_op)
        BusOpRead: target_mode_rd_item_port.write(full_item);
        BusOpWrite: target_mode_wr_item_port.write(full_item);
        default:;
      endcase
      // Hold onto a handle to the previous item (we're about to clear the working item)
      // This is then used to determine how the previous transfer ended (STOP or RSTART)
      prev_item = full_item;
    end

  endtask: target_collect_thread


  virtual protected task target_address_thread();
    i2c_item clone_item;
    bit rw_req = 1'b0;

    cfg.start_perf_monitor.trigger();

    // sample address and r/w bit
    // Only 7-bit addressing is supported.
    for (int i = Addr7BitMode - 1; i >= 0; i--) begin
      cfg.vif.get_bit_data("host", cfg.timing_cfg, mon_dut_item.addr[i]);
      `uvm_info(`gfn, $sformatf("target_address_thread() address[%0d] %b",
        i, mon_dut_item.addr[i]), UVM_DEBUG)
    end
    `uvm_info(`gfn, $sformatf("target_address_thread(), address %0x", mon_dut_item.addr), UVM_DEBUG)
    cfg.vif.get_bit_data("host", cfg.timing_cfg, rw_req);
    `uvm_info(`gfn, $sformatf("target_address_thread(): rw %d", rw_req), UVM_DEBUG)

    cfg.stop_perf_monitor.trigger();

    mon_dut_item.dir = rw_e'(rw_req);
    mon_dut_item.bus_op = (mon_dut_item.dir == i2c_pkg::READ) ? BusOpRead : BusOpWrite;
    mon_dut_item.state = i2c_agent_pkg::StAddrByteRcvd; // Signal the addr+dir is captured

    // get ack after transmitting address
    cfg.vif.wait_for_device_ack_or_nack(cfg.timing_cfg, mon_dut_item.addr_ack);
    if (mon_dut_item.addr_ack == i2c_pkg::NACK) cfg.got_nack.trigger();
    `uvm_info(`gfn,
              $sformatf("target_address_thread(): %0s", mon_dut_item.addr_ack.name()),
              UVM_DEBUG)
    mon_dut_item.state = i2c_agent_pkg::StAddrByteAckRcvd;

    `uvm_info(`gfn, "target_address_thread() detected ACK", UVM_FULL)
  endtask : target_address_thread

  virtual protected task target_read_thread();
    i2c_item clone_item;
    bit [7:0] mon_data;

    mon_dut_item.stop   = 1'b0;
    mon_dut_item.rstart_back = 1'b0;

    while (!mon_dut_item.stop && !mon_dut_item.rstart_back) begin

      mon_dut_item.state = i2c_agent_pkg::StDataByte;

      cfg.start_perf_monitor.trigger();

      // sample read data
      for (int i = 7; i >= 0; i--) begin
        cfg.vif.get_bit_data("device", cfg.timing_cfg, mon_data[i]);
        `uvm_info(`gfn, $sformatf("target_read_thread() trans %0d, byte %0d, bit[%0d] %0b",
            mon_dut_item.tran_id, mon_dut_item.num_data+1, i, mon_data[i]), UVM_DEBUG)
      end

      cfg.stop_perf_monitor.trigger();

      mon_dut_item.data_q.push_back(mon_data);
      mon_dut_item.num_data++;
      mon_dut_item.state = i2c_agent_pkg::StDataByteRcvd;
      `uvm_info(`gfn, $sformatf("target_read_thread() trans %0d, byte %0d 0x%0x",
          mon_dut_item.tran_id, mon_dut_item.num_data, mon_data), UVM_FULL)

      // Sample ACK/NACK
      begin
        i2c_pkg::acknack_e acknack_bit;
        `uvm_info(`gfn, "target_read_thread() waiting for ACK/NACK bit...", UVM_DEBUG)
        cfg.vif.wait_for_host_ack_or_nack(cfg.timing_cfg, acknack_bit);
        if (acknack_bit == i2c_pkg::NACK) cfg.got_nack.trigger();
        mon_dut_item.data_ack_q.push_back(acknack_bit);
        `uvm_info(`gfn, $sformatf("target_read_thread() saw %0s", acknack_bit.name()), UVM_HIGH)
        mon_dut_item.state = i2c_agent_pkg::StDataByteAckRcvd;
      end

      // If we saw NACK, it must be followed by a STOP or RSTART condition.
      if (mon_dut_item.data_ack_q[$] == i2c_pkg::NACK) begin
        cfg.vif.wait_for_host_stop_or_rstart(cfg.timing_cfg,
                                             mon_dut_item.rstart_back,
                                             mon_dut_item.stop);
        `uvm_info(`gfn, $sformatf("target_read_thread() detected %0s",
          (mon_dut_item.stop) ? "STOP" : "RSTART"), UVM_FULL)
      end
    end
  endtask : target_read_thread

  virtual protected task target_write_thread();
    i2c_item clone_item;
    bit [7:0] mon_data;

    mon_dut_item.stop   = 1'b0;
    mon_dut_item.rstart_back = 1'b0;
    `uvm_info(`gfn, $sformatf("target_write_thread() begin: tran_id:%0d num_data%0d",
                              mon_dut_item.tran_id, mon_dut_item.num_data), UVM_FULL)

    while (!mon_dut_item.stop && !mon_dut_item.rstart_back) begin
      fork
        begin : iso_fork_write
          fork
            begin : write_data_collection_thread

              mon_dut_item.state = i2c_agent_pkg::StDataByte;

              cfg.start_perf_monitor.trigger();

              for (int i = 7; i >= 0; i--) begin
                cfg.vif.get_bit_data("host", cfg.timing_cfg, mon_data[i]);
              end

              cfg.stop_perf_monitor.trigger();

              `uvm_info(`gfn, $sformatf("target_write_thread() collected data %0x",
                mon_data), UVM_DEBUG)
              mon_dut_item.num_data++;
              mon_dut_item.data_q.push_back(mon_data);
              `uvm_info(`gfn, $sformatf("target_write_thread() data %2x num_data:%0d",
                                        mon_data, mon_dut_item.num_data), UVM_FULL)

              mon_dut_item.state = i2c_agent_pkg::StDataByteRcvd;

              // Sample the ACK/NACK bit
              begin
                i2c_pkg::acknack_e acknack_bit;
                `uvm_info(`gfn, "controller_write_thread() waiting for ACK/NACK bit...", UVM_DEBUG)
                cfg.vif.wait_for_device_ack_or_nack(cfg.timing_cfg, acknack_bit);
                mon_dut_item.data_ack_q.push_back(acknack_bit);
                if (acknack_bit == i2c_pkg::NACK) cfg.got_nack.trigger();
                `uvm_info(`gfn, $sformatf("target_write_thread() saw %0s",
                                          acknack_bit.name()), UVM_HIGH)
                mon_dut_item.state = i2c_agent_pkg::StDataByteAckRcvd;
              end
            end
            begin : end_of_transfer_thread
              cfg.vif.wait_for_host_stop_or_rstart(cfg.timing_cfg,
                                                   mon_dut_item.rstart_back,
                                                   mon_dut_item.stop);
              `uvm_info(`gfn, $sformatf("target_write_thread() detected %0s %0b",
                (mon_dut_item.stop) ? "STOP" : "RSTART", mon_dut_item.stop), UVM_FULL)
            end
          join_any
          disable fork;
        end : iso_fork_write
      join
    end
    `uvm_info(`gfn, $sformatf("target_write_thread() end: tran_id:%0d num_data:%0d",
                              mon_dut_item.tran_id, mon_dut_item.num_data), UVM_FULL)
  endtask : target_write_thread

  // update of_to_end to prevent sim finished when there is any activity on the bus
  // ok_to_end = 0 (bus busy) / 1 (bus idle)
  virtual task monitor_ready_to_end();
    if (cfg.if_mode == Host) begin
      int scl_cnt = 0;
      if (cfg.en_monitor) begin
        ok_to_end = 0;
      end
      forever begin
        @(cfg.vif.cb);
        if (cfg.vif.scl_i) scl_cnt++;
        else scl_cnt = 0;
        if (scl_cnt > 100) ok_to_end = 1;
      end
    end else begin
      forever begin
        @(cfg.vif.scl_i or cfg.vif.sda_i or cfg.vif.scl_o or cfg.vif.sda_o);
        ok_to_end = (cfg.vif.scl_i == 1'b1) && (cfg.vif.sda_i == 1'b1);
      end
    end
  endtask : monitor_ready_to_end


  // This task is called in a loop to collect I2C Agent-Controller transfers.
  //
  virtual protected task controller_collect_thread();

    // Wait for the driver/vseq to clear this at the start of the next stimulus round.
    wait(cfg.got_stop == 0);

    `uvm_create_obj(i2c_item, mon_dut_item);

    // Publish a handle of the in-progress transfer via the in_progress port.
    // This can be used to make fine-grained predictions based on the ongoing state of the bus,
    // such as by a predictor/reference_model.
    // Note. that any consumers of this item should not modify it.
    controller_mode_in_progress_port.write(mon_dut_item);

    // Decode the start of the next transfer. As we are calling this task in a loop, this may be
    // the start of a new transaction, or the start of new transfer following a repeated-start
    // condition.

    if (prev_item != null && prev_item.rstart_back) begin

      // The previous transaction ended with an RSTART
      mon_dut_item.rstart_front = 1'b1;

    end else if (// Wait for the next START condition if...
                 // - This is the first transaction we are capturing, OR...
                 prev_item == null ||
                 // - The previous captured transaction ended with a STOP.
                 prev_item.stop) begin

      `uvm_info(`gfn, "controller_collect_thread(): Waiting for START condition.", UVM_FULL)
      cfg.vif.wait_for_host_start(cfg.timing_cfg);
      `uvm_info(`gfn, "controller_collect_thread(): Saw START condition.", UVM_FULL)

      mon_dut_item.start = 1'b1;

    end else begin
      // If we got to the else-condition, it means our decoding above failed, and somehow the
      // items are in an unexpected state. This may be a testbench bug.

      `uvm_fatal(`gfn, "State at start of controller_collect_thread() is unexpected!")
    end

    mon_dut_item.state = i2c_agent_pkg::StStarted;
    mon_dut_item.tran_id = num_dut_tran++;

    // Capture the transfer
    begin
      controller_address_thread();
      // We may have seen an early RSTART/STOP during the address+direction byte. This is not a
      // normal transfer, but is valid bus behaviour. If so, push the transfer item normally, then
      // return to the start for the next one.
      if (!mon_dut_item.stop && !mon_dut_item.rstart_back) begin
        case (mon_dut_item.bus_op)
          BusOpRead: controller_read_thread();
          BusOpWrite: controller_write_thread();
          default: `uvm_fatal(`gfn, "Should never get here!")
        endcase
      end
    end

    mon_dut_item.state = i2c_agent_pkg::StStopped;

    // If the previous transfer ended via STOP (as opposed to RSTART), signal this to the driver.
    if (mon_dut_item.stop) cfg.got_stop = 1;

    // The transfer has ended. Send the completed item to the scoreboard.
    begin
      i2c_item full_item;
      `downcast(full_item, mon_dut_item.clone());
      case (full_item.bus_op)
        BusOpRead:  controller_mode_rd_item_port.write(full_item);
        BusOpWrite: controller_mode_wr_item_port.write(full_item);
        default:;
      endcase
      // Hold onto a handle to the previous item (we're about to clear the working item)
      // This is then used to determine how the previous transfer ended (STOP or RSTART)
      prev_item = full_item;
    end

  endtask: controller_collect_thread


  task controller_address_thread();

    fork begin : iso_fork
      fork
        begin : address_capture_thread
          // Does this transfer match one of the DUT's configured addresses?
          bit dut_valid_addr;

          // Collect the 7 address bits. (Only 7-bit Addressing mode is supported)
          for (int i = Addr7BitMode - 1; i >= 0; i--) begin
            cfg.vif.p_edge_scl();
            mon_dut_item.addr[i] = cfg.vif.cb.sda_i;
            `uvm_info(`gfn, $sformatf("controller_address_thread() address[%0d] %b",
                                      i, mon_dut_item.addr[i]), UVM_DEBUG)
          end
          dut_valid_addr = cfg.is_target_addr(mon_dut_item.addr);

          // Collect the R/W bit
          begin
            cfg.vif.p_edge_scl();
            mon_dut_item.dir = (cfg.vif.cb.sda_i == 1'b1) ? i2c_pkg::READ : i2c_pkg::WRITE;
            mon_dut_item.bus_op = (mon_dut_item.dir == i2c_pkg::READ) ? BusOpRead : BusOpWrite;
          end

          `uvm_info(`gfn, $sformatf(
            "controller_address_thread() got byte 0x%2x (address=8'h%2x (%0s), rw=1'b%b (%0s))",
            {mon_dut_item.addr, mon_dut_item.dir},
            mon_dut_item.addr, dut_valid_addr ? "VALID" : "INVALID",
            mon_dut_item.dir, mon_dut_item.dir.name()), UVM_HIGH)

          if (mon_dut_item.bus_op == BusOpRead) begin
            // Push a record of if each transfer had a valid/invalid address to this queue.
            // This is used by the env's scoreboard to form its expectations.
            `uvm_info(`gfn, $sformatf("Pushing back %0s flag to cfg.read_addr_q",
              dut_valid_addr ? "VALID" : "INVALID"), UVM_HIGH)
            cfg.read_addr_q.push_back(dut_valid_addr);
          end

          // The address was valid, collect the ACK/NACK bit...
          cfg.vif.p_edge_scl();
          mon_dut_item.addr_ack = (cfg.vif.cb.sda_i == 1'b1) ? i2c_pkg::NACK : i2c_pkg::ACK;
          `uvm_info(`gfn, $sformatf("controller_address_thread() saw %0s",
                                    mon_dut_item.addr_ack.name()), UVM_HIGH)
          if (mon_dut_item.addr_ack == i2c_pkg::NACK) cfg.got_nack.trigger();
          mon_dut_item.state = i2c_agent_pkg::StAddrByteAckRcvd; // Signal the addr_ack is captured

        end : address_capture_thread

        begin: end_of_transfer_thread
          cfg.vif.wait_for_host_stop_or_rstart(cfg.timing_cfg,
            mon_dut_item.rstart_back, mon_dut_item.stop);
          `uvm_info(`gfn, $sformatf("controller_read_thread(), detected %0s",
            (mon_dut_item.stop) ? "STOP" : "RSTART"), UVM_HIGH)
        end
      join_any
      disable fork;
    end : iso_fork join
  endtask: controller_address_thread


  task controller_read_thread();

    fork begin: iso_fork
      fork
        forever begin : read_data_collection_thread

          mon_dut_item.state = i2c_agent_pkg::StDataByte;

          // Sample the read data
          begin : get_read_bits
            bit [7:0] data;
            for (int i = 7; i >= 0; i--) begin
              cfg.vif.p_edge_scl();
              data[i] = cfg.vif.sda_i;
            end
            `uvm_info(`gfn, $sformatf("controller_read_thread() got data 0x%2x", data), UVM_HIGH)
            // Push read data into the item
            mon_dut_item.data_q.push_back(data);
            mon_dut_item.num_data++;
            mon_dut_item.state = i2c_agent_pkg::StDataByteRcvd;
          end

          // Collect the ACK/NACK bit...
          begin
            i2c_pkg::acknack_e acknack_bit;
            cfg.vif.wait_for_host_ack_or_nack(cfg.timing_cfg, acknack_bit);
            mon_dut_item.data_ack_q.push_back(acknack_bit);
            if (acknack_bit == i2c_pkg::NACK) cfg.got_nack.trigger();
            `uvm_info(`gfn, $sformatf("controller_read_thread(), detected %0s",
                                      acknack_bit.name()), UVM_HIGH)
            mon_dut_item.state = i2c_agent_pkg::StDataByteAckRcvd;
          end

          cfg.rcvd_rd_byte++;

          `uvm_info(`gfn, $sformatf("controller_read_thread(), trans %0d, byte_num %0d, 8'h%2x",
             mon_dut_item.tran_id, mon_dut_item.num_data, mon_dut_item.data_q[$]), UVM_HIGH)

          // If we saw a NACK, it must be followed by a STOP or RSTART condition
          // This is awaited in the 'end_of_transfer_thread'
        end : read_data_collection_thread

        begin: end_of_transfer_thread
          cfg.vif.wait_for_host_stop_or_rstart(cfg.timing_cfg,
            mon_dut_item.rstart_back, mon_dut_item.stop);
          `uvm_info(`gfn, $sformatf("controller_read_thread(), detected %0s",
            (mon_dut_item.stop) ? "STOP" : "RSTART"), UVM_HIGH)

          // TODO. Move to reference_model/predictor.
          // In ack_stop test mode, the agent may generate stimulus that sends a STOP condition
          // immediately after ack'ing a read byte. In this case, we predict the DUT will raise the
          // 'unexp_stop' interrupt. Set 'cfg.ack_stop_det' to indicate this stimulus has occured.
          if (cfg.allow_ack_stop) begin
            if (mon_dut_item.stop && (mon_dut_item.data_ack_q[$] == i2c_pkg::ACK)) begin
              cfg.ack_stop_det = 1;
              `uvm_info(`gfn, "Observed ack-then-stop condition now.", UVM_MEDIUM)
            end
          end
        end

      join_any
      disable fork;
    end: iso_fork join

  endtask: controller_read_thread


  task controller_write_thread();
    fork begin: iso_fork
      fork
        forever begin: write_data_collection_thread

          mon_dut_item.state = i2c_agent_pkg::StDataByte;

          // Sample the write data
          begin : get_write_bits
            bit [7:0] data;
            for (int i = 7; i >= 0; i--) begin
              cfg.vif.p_edge_scl();
              data[i] = cfg.vif.sda_i;
            end
            `uvm_info(`gfn, $sformatf("controller_write_thread() got data 0x%2x", data), UVM_HIGH)
            // Push write data into the item
            mon_dut_item.data_q.push_back(data);
            mon_dut_item.num_data++;
            mon_dut_item.state = i2c_agent_pkg::StDataByteRcvd;
          end

          // Collect the ACK/NACK bit...
          begin : get_ack_nack_bit
            i2c_pkg::acknack_e acknack_bit;
            cfg.vif.wait_for_host_ack_or_nack(cfg.timing_cfg, acknack_bit);
            mon_dut_item.data_ack_q.push_back(acknack_bit);
            if (acknack_bit == i2c_pkg::NACK) cfg.got_nack.trigger();
            `uvm_info(`gfn, $sformatf("controller_write_thread(), detected %0s",
                                      acknack_bit.name()), UVM_HIGH)
            mon_dut_item.state = i2c_agent_pkg::StDataByteAckRcvd;
          end

          `uvm_info(`gfn, $sformatf("controller_write_thread(), trans %0d, byte_num %0d, 8'h%2x",
             mon_dut_item.tran_id, mon_dut_item.num_data, mon_dut_item.data_q[$]), UVM_HIGH)

          // If the target NACKs a controller write byte, the controller should
          // immediately send a stop or rstart condition, ending the transfer.
          // However, a P/Sr condition at any point is valid to end the transfer. The
          // 'end_of_transfer_thread' below will terminate this fork-join block for both
          // of these cases.
        end

        begin: end_of_transfer_thread
          // STOP / RSTART can occur at any time, ending the transfer. Await those here.
          cfg.vif.wait_for_host_stop_or_rstart(
            cfg.timing_cfg, mon_dut_item.rstart_back, mon_dut_item.stop);
          `uvm_info(`gfn, $sformatf("controller_write_thread() saw %0s condition.",
            (mon_dut_item.stop) ? "STOP" : "RSTART"), UVM_HIGH)
        end

      join_any
      disable fork;
    end: iso_fork join
  endtask: controller_write_thread


endclass : i2c_monitor
